﻿//==============================================================================
// File: ValidatableObservableCollection.cs
// Created: 2010-05-10
// Author: Piotr Włodek
//==============================================================================
// This file is a part of MAMMOTH project.
// Copyright (C) 2010 AGH University of Science and Technology, Krakow.
// https://caribou.iisg.agh.edu.pl/trac/mammoth
//
// Mammoth is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// 
// Mammoth is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Lesser General Public License for more details.
// 
// You should have received a copy of the GNU Lesser General Public License
// along with WpfRcp. If not, see http://www.gnu.org/licenses/.
//==============================================================================

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using WpfRcp.PropertyModel.Core;
using WpfRcp.PropertyModel.Validation;

namespace WpfRcp.PropertyModel.Collections
{
    /// <summary>
    /// Observable collection that supports <see cref="IValidatable"/> and
    /// <see cref="INotifyObjectStateChanged"/> interfaces.
    /// </summary>
    /// <typeparam name="T">Item's type.</typeparam>
    public class ValidatableObservableCollection<T> : ObservableCollection<T>, IValidatable, INotifyObjectStateChanged
    {
        private readonly LinkedList<IValidatable> m_InvalidItems = new LinkedList<IValidatable>();
        private bool m_IsValid = true;

        /// <summary>
        /// Initializes a new instance of the <see cref="ValidatableObservableCollection{T}"/> class.
        /// </summary>
        public ValidatableObservableCollection()
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="ValidatableObservableCollection{T}"/> class.
        /// </summary>
        /// <param name="list">Initial items.</param>
        public ValidatableObservableCollection(List<T> list) : base(list)
        {
            Initialize(list);
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="ValidatableObservableCollection{T}"/> class.
        /// </summary>
        /// <param name="collection">Initial items.</param>
        public ValidatableObservableCollection(IEnumerable<T> collection) : base(collection)
        {
            Initialize(collection);
        }

        /// <summary>
        /// Gets an item from specified index. If the index is out of the bound, no exception is thrown.
        /// </summary>
        /// <param name="index">Index from which the item is being returned.</param>
        /// <returns>An item from specified index.</returns>
        public T TryGet(int index)
        {
            T item = default(T);
            if (index >= 0 && index < Count)
                item = this[index];

            return item;
        }

        #region Overrides

        protected override void ClearItems()
        {
            foreach (var item in this)
            {
                DetachItem(item);
            }

            base.ClearItems();

            Debug.Assert(m_InvalidItems.Count == 0);
            OnObjectStateChanged();
        }
        
        protected override void RemoveItem(int index)
        {
            T item = TryGet(index);
            base.RemoveItem(index);

            DetachItem(item);
            OnObjectStateChanged();
        }

        protected override void InsertItem(int index, T item)
        {
            base.InsertItem(index, item);

            AttachItem(item);
            OnObjectStateChanged();
        }

        protected override void SetItem(int index, T newItem)
        {
            T oldItem = TryGet(index);
            base.SetItem(index, newItem);

            DetachItem(oldItem);
            AttachItem(newItem);

            OnObjectStateChanged();
        }

        #endregion

        #region Private Validation

        private void Initialize(IEnumerable<T> original)
        {
            foreach (var item in original)
            {
                AttachItem(item);
            }
            
            OnObjectStateChanged();
        }

        private void AttachItem(T item)
        {
            if (item is IValidatable && item is INotifyObjectStateChanged)
            {
                var validatableItem = (IValidatable) item;
                 
                if (!validatableItem.IsValid)
                {
                    m_InvalidItems.AddLast(validatableItem);
                }

                var notifiableItem = (INotifyObjectStateChanged)item;
                notifiableItem.ObjectStateChanged += ValidatableItemObjectStateChanged;
            }
        }

        private void DetachItem(T item)
        {
            if (item is IValidatable && item is INotifyObjectStateChanged)
            {
                var validatableItem = (IValidatable)item;

                if (!validatableItem.IsValid)
                {
                    bool removed = m_InvalidItems.Remove(validatableItem);
                    Debug.Assert(removed);
                }

                var notifiableItem = (INotifyObjectStateChanged)item;
                notifiableItem.ObjectStateChanged -= ValidatableItemObjectStateChanged;
            }
        }

        private void ValidatableItemObjectStateChanged(object sender, EventArgs e)
        {
            var validatableItem = (IValidatable) sender;

            if (validatableItem.IsValid)
            {
                m_InvalidItems.Remove(validatableItem);
            }
            else if (!m_InvalidItems.Contains(validatableItem))
            {
                m_InvalidItems.AddLast(validatableItem);
            }

            OnObjectStateChanged();
        }

        #endregion

        #region IValidatable Implementation

        /// <summary>
        /// Gets a value indicating whether object is in valid state.
        /// </summary>
        public bool IsValid
        {
            get { return m_IsValid; }
            private set
            {
                if (m_IsValid != value)
                {
                    m_IsValid = value;
                    OnPropertyChanged(new System.ComponentModel.PropertyChangedEventArgs("IsValid"));
                }
            }
        }

        #endregion

        #region INotifyObjectStateChanged Implementation

        /// <summary>
        /// Event raised whenever object's state changes.
        /// </summary>
        public event EventHandler<EventArgs> ObjectStateChanged;

        protected virtual void OnObjectStateChanged()
        {
            IsValid = m_InvalidItems.Count == 0;

            if (ObjectStateChanged != null)
                ObjectStateChanged(this, EventArgs.Empty);
        }

        #endregion
    }
}
